#!/usr/bin/python

# Copyright (C) 2012 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# This code is not supported by Google


"""
Script to change the price and/or availability settings for your items in 
Merchant Center using the express update mechanism:

https://developers.google.com/commerce-search/docs/feeds#processing

Normal feed processing may take up to 24 hours but if you *just* change the
price or availability of an existing item, the updates go though to serving
much faster.  The trick is to update all the fields in an already serving items
exactly except for the price or availability settings.

This script accepts a list of itemID numbers and the new price/availability 
settings.  

Each item in the list is downloaded from Merchant Center usign the ContentAPI-batch-query
protocol (which ensures that all the fields you uploaded earlier is returned).

For each item, either the same data that was downloaded using the query is 
again uploaded with the new price or availability set.


REFERENCE : 
https://developers.google.com/shopping-content/getting-started/usingapi-products
http://code.google.com/p/gcs-admin-toolkit/wiki/GCSAuthentication
https://developers.google.com/accounts/docs/OAuth2#serviceaccount
http://code.google.com/p/gcs-admin-toolkit/
http://code.google.com/p/gcs-admin-toolkit/source/browse/trunk/src/auth/

USAGE:
    (first get an oauth2 private key and associate the oauth2 as an 
    administrator to your MC instance).  Specify your merchantID as the second param
    Acquire an oauth2 bearer token and call the update function.
    NOTE: oauth tokens expire after 1hr
    
    e = express('1/p-AEol_0T1dh_IOGRw8xqaLmkn1mdofOyN74-9JZkvI','8437442')    
    item_to_update = [('online:en:us:3','130.00'), ('online:en:us:32123','out of stock')]    
    e.update(item_to_update)


"""

import sys
import getopt
import time
import sys
import logging
import urllib,urllib2
from urllib2 import URLError, HTTPError
import libxml2
import re
import sys

from datetime import datetime

class express(object):
  
  # list of availabilities you can set
  VALID_AVAILABILITIES = ['in stock', 'out of stock', 'limited availability', 'available for order', 'preorder']

  def __init__(self, token, cid):
    self.log("Express Update using token: " + token, logging.INFO)
    self.AUTH_TOKEN = token
    self.cid = cid

    
  def update(self, items_to_update):
    
    # the batch protocol only accepts upto 1MB worth of data
    # but i'm arbitrarily setting the limit per request to 100
    
    if len(items_to_update) > 100:
      self.log("Number of items to update per batch must be less than 100", logging.ERROR)
      sys.exit(0)
        
    batch_template_start = "<feed xmlns='http://www.w3.org/2005/Atom' xmlns:batch='http://schemas.google.com/gdata/batch'>"
    batch_template_end = "</feed>"
    
    query_string = ''
    
    now = datetime.utcnow()

    # for each item in the list, create a batch QUERY stanza
    for item in items_to_update:
      query_string =  query_string + ('<entry><batch:operation type="QUERY"/><batch:id>' + item[0] + '</batch:id>' + 
          '<id>http://content.apis.google.com/content/v1/' + self.cid + '/items/products/schema/' + item[0] + '</id></entry>')
    query_string = batch_template_start + query_string + batch_template_end

    url = 'https://content.googleapis.com/content/v1/' + self.cid +'/items/products/schema/batch' 
    headers = {"GData-Version": "1", "Content-type": "application/atom+xml"}
    
    # for ClientLogin
    # https://developers.google.com/accounts/docs/AuthForInstalledApps
    # see http://code.google.com/p/gcs-admin-toolkit/source/browse/trunk/src/python/fetch-products.py#56
    #headers = {"GData-Version": "1", "Content-type": "application/atom+xml", "Authorization": "GoogleLogin auth=" + self.AUTH_TOKEN}
    
    # for oauth2 
    # https://developers.google.com/accounts/docs/OAuth2#serviceaccount
    # NOTE: oauth2 tokens are only valid for 1 hour.  Expired tokens should get renewed
    # set SCOPE=https://www.googleapis.com/auth/structuredcontent
    # see http://code.google.com/p/gcs-admin-toolkit/source/browse/trunk/src/auth/gcs_auth.py
    url = url + '?access_token=' + self.AUTH_TOKEN
    req = urllib2.Request(url, query_string, headers)

    try:
      content = urllib2.urlopen(req).read()
      self.log('Query Response : ' + content,  logging.DEBUG)  

      # parse the xml and create namespaces we'll need later
      doc = libxml2.parseDoc(content)
      ctxt = doc.xpathNewContext()
      ctxt.xpathRegisterNs('openSearch', "http://a9.com/-/spec/opensearchrss/1.0/")
      ctxt.xpathRegisterNs('s', "http://www.google.com/shopping/api/schemas/2010")  
      ctxt.xpathRegisterNs('atom', "http://www.w3.org/2005/Atom")  
      ctxt.xpathRegisterNs('app', "http://www.w3.org/2007/app")
      ctxt.xpathRegisterNs('gd', "http://schemas.google.com/g/2005")
      ctxt.xpathRegisterNs('sc', "http://schemas.google.com/structuredcontent/2009")
      ctxt.xpathRegisterNs('scp', "http://schemas.google.com/structuredcontent/2009/products")
      ctxt.xpathRegisterNs('batch',"http://schemas.google.com/gdata/batch")
      
      # atom:entry defines each item
      nl_offers = ctxt.xpathEval('/atom:feed/atom:entry')
      
      for n_offer in nl_offers:
        # reset the xpath contenxt to the entry
        ctxt.setContextNode(n_offer)
        
        id = urllib2.unquote(ctxt.xpathEval('atom:id')[0].content)
        
        # if the itemID isn't found in Merchant Center, the batch response for
        # that item will be 404 not found
        batch_id = urllib2.unquote(ctxt.xpathEval('batch:id')[0].content)
        n_batch = ctxt.xpathEval('batch:status')[0]
        for prop in n_batch.properties:
          if prop.type == 'attribute' and prop.name.lower() == 'reason' and prop.content.lower() == 'item not found':
            self.log(str('Item not found:: [' + batch_id + '] '),  logging.info)      
            n_offer.unlinkNode()
            n_offer.freeNode()  

        for item in items_to_update:
          
          canonical_id = 'http://content.googleapis.com/content/v1/' + self.cid + '/items/products/schema/' + item[0]
          
          # iterate through the list of items retruned vs. the update list specified 
          if canonical_id.lower() == id.lower():           
            pid = ctxt.xpathEval('sc:id')[0].content
            #ctxt.xpathEval('atom:id')[0].setContent(pid)
            
            # remove extra parameters that the content api always returns
            published = ctxt.xpathEval('atom:published')[0]            
            published.unlinkNode()
            published.freeNode()
            
            updated = ctxt.xpathEval('atom:updated')[0]            
            updated.unlinkNode()
            updated.freeNode()            

            last_edited  = ctxt.xpathEval('app:edited')[0]
            last_edited.unlinkNode()
            last_edited.freeNode()  
            
            expiration_date  = ctxt.xpathEval('sc:expiration_date')[0]
            expiration_date.unlinkNode()
            expiration_date.freeNode()     
            
            batch_status  = ctxt.xpathEval('batch:status')[0]
            batch_status.unlinkNode()
            batch_status.freeNode()     
            
            # uncomment to always include Google Commerce Search
            #n_appcontrol = ctxt.xpathEval('app:control')
            #req_dest = libxml2.newNode('sc:required_destination')
            #req_dest.setProp('dest','CommerceSearch')
            #n_appcontrol[0].addChild(req_dest)               
                        
            nl_status  = ctxt.xpathEval('app:control/sc:status')
            for st in nl_status:
              dest = st.prop('dest')
              status = st.prop('status')
              #self.log(str('Status :: [' + dest + '] ' + status),  logging.info)
              if (dest == 'CommerceSearch' and status == 'enlisted'):
                req_dest = libxml2.newNode('sc:required_destination')
                req_dest.setProp('dest','CommerceSearch')
                n_appcontrol = ctxt.xpathEval('app:control')
                n_appcontrol[0].addChild(req_dest)                      
              st.unlinkNode()
              st.freeNode()          
            
            nl_links  = ctxt.xpathEval('atom:link')
            for link in nl_links:
              for prop in link.properties:
                if prop.name.lower() == 'rel' and prop.content.lower() != 'alternate':
                  link.unlinkNode()
                  link.freeNode()
            
            # now update either the availability or price
            if (item[1] in self.VALID_AVAILABILITIES):
              n_availability = ctxt.xpathEval('scp:availability')
              n_availability[0].setContent(item[1])              
            else:     
              n_price = ctxt.xpathEval('scp:price')
              n_price[0].setContent(item[1])
      
            # change the batch operation to 'update'
            batch = ctxt.xpathEval('batch:operation')[0]
            for prop in batch.properties:
              if prop.name.lower() == 'type' and prop.content.lower() == 'query':
                prop.content = 'update'
                batch.setProp('type', 'update')
      
      update_string =  doc.serialize('UTF-8')
      
      #self.log('Update Request : ' + update_string,  logging.DEBUG)
      
      # TODO: ?dry-run
      # TODO: validate_destination on use_cases
      
      # now post the update with the modified XML
      
      url = 'https://content.googleapis.com/content/v1/' + self.cid +'/items/products/schema/batch' 
      headers = {"GData-Version": "1", "Content-type": "application/atom+xml"}
      url = url + '?access_token=' + self.AUTH_TOKEN
      req = urllib2.Request(url, update_string, headers)
      content = urllib2.urlopen(req).read()
      #self.log('Update Repsonse : ' + content,  logging.DEBUG)

      # optionally parse out the response codes
      doc = libxml2.parseDoc(content)
      ctxt = doc.xpathNewContext()
      ctxt.xpathRegisterNs('atom', "http://www.w3.org/2005/Atom")  
      ctxt.xpathRegisterNs('batch',"http://schemas.google.com/gdata/batch")       
      nl_offers = ctxt.xpathEval('/atom:feed/atom:entry')
      for n_offer in nl_offers:
        ctxt.setContextNode(n_offer)
        id = urllib2.unquote(ctxt.xpathEval('batch:id')[0].content)
        n_batch = ctxt.xpathEval('batch:status')[0]
        for prop in n_batch.properties:
                if prop.type == 'attribute' and prop.name.lower() == 'reason':
                    self.log(str('Update Status: [' + id + '] ' + prop.content ),  logging.info)          
      
      ctxt.xpathFreeContext()
      doc.freeDoc()

    except HTTPError, e:
      self.log('HTTPError code: ' + str(e.code),logging.ERROR)
      self.log(e.read(),logging.ERROR)
      sys.exit(1)
    except URLError, e:
      self.log( 'URLError: ' + str(e.reason),logging.ERROR)
      self.log(e.read(),logging.ERROR)
      sys.exit(1)
    except Exception, e:
      self.log( 'Error: ' + str(e),logging.ERROR)
      sys.exit(1)
            
  def log(self,msg, loglevel):
    #LOG_FILENAME = 'express.log'
    #logging.basicConfig(filename=LOG_FILENAME,level=logging.INFO)
    m = ('[%s] %s') % (datetime.now(), msg)
    print m
    if (loglevel == logging.DEBUG):
      logging.debug(m)   
    else:
      logging.info(m)
               
if __name__ == '__main__':
    e = express('<YOUR AUTH TOKEN>','<YOUR MERCHANT ID>')
    
    item_to_update = [('online:en:us:<ITEM1 ID>','130.00'), ('online:en:us:<ITEM2 ID>','in stock')]
    
    e.update(item_to_update)